// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

library dart2js.js_backend.patch_resolver;

import '../common.dart';
import '../common/resolution.dart' show Resolution;
import '../common/tasks.dart' show CompilerTask;
import '../compiler.dart' show Compiler;
import '../elements/resolution_types.dart';
import '../elements/elements.dart';
import '../elements/modelx.dart';
import '../tree/tree.dart';

class PatchResolverTask extends CompilerTask {
  final Compiler compiler;
  PatchResolverTask(Compiler compiler)
      : compiler = compiler,
        super(compiler.measurer);

  DiagnosticReporter get reporter => compiler.reporter;

  Resolution get resolution => compiler.resolution;

  String get name => 'JavaScript patch resolver';

  FunctionElement resolveExternalFunction(FunctionElementX element) {
    if (element.isPatched) {
      FunctionElementX patch = element.patch;
      reporter.withCurrentElement(patch, () {
        patch.computeType(resolution);
      });
      checkMatchingPatchSignatures(element, patch);
      element = patch;
    } else {
      if (element.isConstructor) {
        // Note: currently we allow a couple external methods without a patch,
        // namely the *.fromEnvironment const constructors in int, bool, and
        // String.  In the future we might also represent native DOM methods in
        // dart:html this way.
        ConstructorElementX constructor = element;
        if (constructor.isFromEnvironmentConstructor) return element;
      }
      reporter.reportErrorMessage(
          element, MessageKind.PATCH_EXTERNAL_WITHOUT_IMPLEMENTATION);
    }
    return element;
  }

  void checkMatchingPatchParameters(FunctionElement origin,
      List<Element> originParameters, List<Element> patchParameters) {
    bool isUnnamedListConstructor = origin is ConstructorElement &&
        resolution.commonElements.isUnnamedListConstructor(origin);

    assert(originParameters.length == patchParameters.length);
    for (int index = 0; index < originParameters.length; index++) {
      ParameterElementX originParameter = originParameters[index];
      ParameterElementX patchParameter = patchParameters[index];
      // TODO(johnniwinther): Remove the conditional patching when we never
      // resolve the same method twice.
      if (!originParameter.isPatched) {
        originParameter.applyPatch(patchParameter);
      } else {
        assert(originParameter.patch == patchParameter,
            failedAt(origin, "Inconsistent repatch of $originParameter."));
      }
      ResolutionDartType originParameterType =
          originParameter.computeType(resolution);
      ResolutionDartType patchParameterType =
          patchParameter.computeType(resolution);
      if (originParameterType != patchParameterType) {
        reporter.reportError(
            reporter.createMessage(
                originParameter, MessageKind.PATCH_PARAMETER_TYPE_MISMATCH, {
              'methodName': origin.name,
              'parameterName': originParameter.name,
              'originParameterType': originParameterType,
              'patchParameterType': patchParameterType
            }),
            <DiagnosticMessage>[
              reporter.createMessage(
                  patchParameter,
                  MessageKind.PATCH_POINT_TO_PARAMETER,
                  {'parameterName': patchParameter.name}),
            ]);
      } else {
        // Hack: Use unparser to test parameter equality. This only works
        // because we are restricting patch uses and the approach cannot be used
        // elsewhere.

        // The node contains the type, so there is a potential overlap.
        // Therefore we only check the text if the types are identical.
        String originParameterText = originParameter.node.toString();
        String patchParameterText = patchParameter.node.toString();
        if (originParameterText != patchParameterText
            // We special case the list constructor because of the
            // optional parameter.
            &&
            !isUnnamedListConstructor) {
          reporter.reportError(
              reporter.createMessage(
                  originParameter, MessageKind.PATCH_PARAMETER_MISMATCH, {
                'methodName': origin.name,
                'originParameter': originParameterText,
                'patchParameter': patchParameterText
              }),
              <DiagnosticMessage>[
                reporter.createMessage(
                    patchParameter,
                    MessageKind.PATCH_POINT_TO_PARAMETER,
                    {'parameterName': patchParameter.name}),
              ]);
        }
      }
    }
  }

  void checkMatchingPatchSignatures(
      FunctionElement origin, FunctionElement patch) {
    // TODO(johnniwinther): Show both origin and patch locations on errors.
    FunctionExpression originTree = origin.node;
    FunctionSignature originSignature = origin.functionSignature;
    FunctionExpression patchTree = patch.node;
    FunctionSignature patchSignature = patch.functionSignature;

    if ('${originTree.typeVariables}' != '${patchTree.typeVariables}') {
      reporter.withCurrentElement(patch, () {
        Node errorNode = patchTree.typeVariables != null
            ? patchTree.typeVariables
            : patchTree;
        reporter.reportError(
            reporter.createMessage(
                errorNode,
                MessageKind.PATCH_TYPE_VARIABLES_MISMATCH,
                {'methodName': origin.name}),
            [reporter.createMessage(origin, MessageKind.THIS_IS_THE_METHOD)]);
      });
    }
    if (originSignature.type.returnType != patchSignature.type.returnType) {
      reporter.withCurrentElement(patch, () {
        Node errorNode =
            patchTree.returnType != null ? patchTree.returnType : patchTree;
        reporter.reportErrorMessage(
            errorNode, MessageKind.PATCH_RETURN_TYPE_MISMATCH, {
          'methodName': origin.name,
          'originReturnType': originSignature.type.returnType,
          'patchReturnType': patchSignature.type.returnType
        });
      });
    }
    if (originSignature.requiredParameterCount !=
        patchSignature.requiredParameterCount) {
      reporter.withCurrentElement(patch, () {
        reporter.reportErrorMessage(
            patchTree, MessageKind.PATCH_REQUIRED_PARAMETER_COUNT_MISMATCH, {
          'methodName': origin.name,
          'originParameterCount': originSignature.requiredParameterCount,
          'patchParameterCount': patchSignature.requiredParameterCount
        });
      });
    } else {
      checkMatchingPatchParameters(origin, originSignature.requiredParameters,
          patchSignature.requiredParameters);
    }
    if (originSignature.optionalParameterCount != 0 &&
        patchSignature.optionalParameterCount != 0) {
      if (originSignature.optionalParametersAreNamed !=
          patchSignature.optionalParametersAreNamed) {
        reporter.withCurrentElement(patch, () {
          reporter.reportErrorMessage(
              patchTree,
              MessageKind.PATCH_OPTIONAL_PARAMETER_NAMED_MISMATCH,
              {'methodName': origin.name});
        });
      }
    }
    if (originSignature.optionalParameterCount !=
        patchSignature.optionalParameterCount) {
      reporter.withCurrentElement(patch, () {
        reporter.reportErrorMessage(
            patchTree, MessageKind.PATCH_OPTIONAL_PARAMETER_COUNT_MISMATCH, {
          'methodName': origin.name,
          'originParameterCount': originSignature.optionalParameterCount,
          'patchParameterCount': patchSignature.optionalParameterCount
        });
      });
    } else {
      checkMatchingPatchParameters(origin, originSignature.optionalParameters,
          patchSignature.optionalParameters);
    }
  }
}
